/**
 * File: GzipCompressor.java
 * Created by: mhaimel
 * Created on: Jun 24, 2011
 * CVS:  $Id: GzipCompressor.java 1.0 Jun 24, 2011 3:11:06 PM mhaimel Exp $
 */
package uk.ac.ebi.curtain.storage;

import java.util.Arrays;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

/**
 * @author mhaimel
 *
 */
public class GzipFormatCompressor implements CompressorStrategy{
	//	private final int blocksize;
    private final CRC32 crc32 = new CRC32();
	private final Deflater deflater; //TODO currently like picard
    private final Inflater inflater = new Inflater(true); // GZIP mode
	private byte[] localBuffer;
	private int blocksize = 0;
    private final static int GZIP_MAGIC = 0x8b1f;
    private final static byte[] header = {
        (byte) GZIP_MAGIC,                // Magic number (short)
        (byte)(GZIP_MAGIC >> 8),          // Magic number (short)
        Deflater.DEFLATED,                // Compression method (CM)
        4,                                // Flags (FLG) - FEXTRA flag for optional fields
        0,                                // Modification time MTIME (int)
        0,                                // Modification time MTIME (int)
        0,                                // Modification time MTIME (int)
        0,                                // Modification time MTIME (int)
        0,                                // Extra flags (XFLG)
        0                                 // Operating system (OS)
    };
	private final static int HEADER_LENGTH = header.length + 2 + 4; // header + extra length size (short) + extra length value int
	private static final int FOOTER_LENGTH = 8;

	public GzipFormatCompressor(int blocksize, int level) {
		setBlockSize(blocksize);
		this.deflater = new Deflater(level, true);
	}
	
	public byte[] compress(byte[] bytes){
		checkBuffer(bytes.length);
		deflater.reset();
        deflater.setInput(bytes, 0, bytes.length);
        deflater.finish();

        int compressedSize = deflater.deflate(this.localBuffer, 0, this.localBuffer.length);
        if(!deflater.finished()){
            throw new IllegalStateException("unpossible");
        }
        byte[] bpackage = wrapPackage(bytes,this.localBuffer,compressedSize);
		return bpackage;
	}
	
	public byte[] decompress(byte[] bytes){
		inflater.reset();
		int cSize = unwrapHeader(bytes);
		
		long expCrc = readUInt(bytes, bytes.length - 8);
		int expLen = (int)readUInt(bytes, bytes.length - 4);
		byte[] nBuff = null;
		try{
			nBuff = new byte[expLen];
		} catch (Exception e) {
			throw new Error("Problems allocating a byte array with "+expLen + " entries!!!",e);
		}
		inflater.setInput(bytes,HEADER_LENGTH,cSize);
		try {
			int uncompressedSize = inflater.inflate(nBuff);
			if(uncompressedSize != expLen){
				throw new IllegalStateException("Inflated size wrong: expected " + expLen+ "; acctual: " + uncompressedSize);
			}
			long acCrc = calcCrc32(nBuff);
			if(acCrc != expCrc){
				throw new IllegalStateException("Inlated crc32 wrong: expected " + expCrc+ "; acctual: " + acCrc);
			}
			return nBuff;
		} catch (DataFormatException e) {
			throw new IllegalStateException(e);
		}
	}
	private int unwrapHeader(byte[] bytes) {
		checkHeader(bytes);
		int rLen = readUShort(bytes, header.length);
		if(rLen != 4){
			throw new IllegalStateException("Format changed - 4 bytes extra field expected!!!");
		}
		return (int) readUInt(bytes, header.length+2);
	}
	private void checkHeader(byte[] bytes) {
		boolean equals = Arrays.equals(header, Arrays.copyOfRange(bytes, 0, 10));
		if(!equals){
			throw new IllegalStateException("Header of block not matching");
		}
	}
	@Override
	public byte[] compressBytes(byte[] source) {
		return this.compress(source);
	}
	@Override
	public byte[] decompressBytes(byte[] source) {
		return this.decompress(source);
	}
	@Override
	public void setBlockSize(int size) {
		this.blocksize = size;
		this.localBuffer = new byte[this.blocksize+10];
	}
	public void checkBlockSize(int size){
		if(this.localBuffer.length < size){
			this.localBuffer = new byte[size];
		}
	}
	
	private long calcCrc32(byte[] uBytes) {
		crc32.reset();
		crc32.update(uBytes);
		return crc32.getValue();
	}
	private void checkBuffer(int length) {
		if(this.localBuffer.length < (length+10)){
			this.localBuffer = new byte[length+10];
		}
	}

	private byte[] wrapPackage(byte[] uBytes, byte[] cBytes,int compressedSize) {
		long crc = calcCrc32(uBytes);
		// 10 header + compressedbytes + 8bytes footer
		byte[] arr = new byte[compressedSize+HEADER_LENGTH+FOOTER_LENGTH];
		writeHeader(arr,compressedSize);
		// fill package
		System.arraycopy(cBytes, 0, arr, HEADER_LENGTH, compressedSize);
		writeTrailer(arr, HEADER_LENGTH+compressedSize, crc, uBytes.length);
		return arr;
	}
	private void writeHeader(byte[] arr, int cSize) {
		int hLen = header.length;
		System.arraycopy(header, 0, arr, 0, hLen);
		writeShort(4, arr, hLen);
		writeInt(cSize, arr, hLen+2);
	}
	
	/*
	 * @see GZIPOutputStream
	 */
	private void writeTrailer(byte[] buf, int offset, long crc, int unSize){
		writeInt((int)crc, buf, offset); // CRC-32 of uncompr. data
        writeInt(unSize, buf, offset + 4); // Number of uncompr. bytes
	}

	/*
	 * Writes integer in Intel byte order to a byte array, starting at a
	 * given offset.
	 * @see GZIPOutputStream
	 */
	private void writeInt(int i, byte[] buf, int offset) {
	    writeShort(i & 0xffff, buf, offset);
	    writeShort((i >> 16) & 0xffff, buf, offset + 2);
	}

	/*
	 * Writes short integer in Intel byte order to a byte array, starting
	 * at a given offset
	 * @see GZIPOutputStream
	 */
	private void writeShort(int s, byte[] buf, int offset) {
	    buf[offset] = (byte)(s & 0xff);
	    buf[offset + 1] = (byte)((s >> 8) & 0xff);
	}
	
    /*
     * Reads unsigned integer in Intel byte order.
     */
    private long readUInt(byte[] buf, int offset) {
	long s = readUShort(buf,offset);
	return ((long)readUShort(buf,offset+2) << 16) | s;
    }

    /*
     * Reads unsigned short in Intel byte order.
     */
    private int readUShort(byte[] buf,int offset){
	int x = readUByte(buf, offset);
	return ((int)readUByte(buf, offset+1) << 8) | x;
    }

    /*
     * Reads unsigned byte.
     */
    private int readUByte(byte[] buf,int offset){
	    int in = buf[offset]& 0xff;
		if (in == -1) {
		    throw new IllegalStateException();
		}
        if (in < -1 || in > 255) {
            // Report on this.in, not argument in; see read{Header, Trailer}.
            throw new IllegalStateException("Byte array offset returned value out of range -1..255: " + offset);
        }
	return in;
    }
		
}
